#!/bin/python

#    This file is part of python-registry.
#
#   Copyright 2011 Will Ballenthin <william.ballenthin@mandiant.com>
#                    while at Mandiant <http://www.mandiant.com>
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

# Added for python2-3 compatibility
from __future__ import print_function
from __future__ import unicode_literals
from __future__ import absolute_import

import struct
import datetime
import decimal
import binascii
from ctypes import c_uint32
from enum import Enum
from collections import namedtuple
from Registry import SettingsParse

# Constants
RegSZ = 0x0001
RegExpandSZ = 0x0002
RegBin = 0x0003
RegDWord = 0x0004
RegMultiSZ = 0x0007
RegQWord = 0x000B
RegNone = 0x0000
RegBigEndian = 0x0005
RegLink = 0x0006
RegResourceList = 0x0008
RegFullResourceDescriptor = 0x0009
RegResourceRequirementsList = 0x000A
RegFileTime = 0x0010
# Following are new types from settings.dat
RegUint8 = 0x101
RegInt16 = 0x102
RegUint16 = 0x103
RegInt32 = 0x104
RegUint32 = 0x105
RegInt64 = 0x106
RegUint64 = 0x107
RegFloat = 0x108
RegDouble = 0x109
RegUnicodeChar = 0x10A
RegBoolean = 0x10B
RegUnicodeString = 0x10C
RegCompositeValue = 0x10D
RegDateTimeOffset = 0x10E
RegTimeSpan = 0x10F
RegGUID = 0x110
RegUnk111 = 0x111
RegUnk112 = 0x112
RegUnk113 = 0x113
RegBytesArray = 0x114
RegInt16Array = 0x115
RegUint16Array = 0x116
RegInt32Array = 0x117
RegUInt32Array = 0x118
RegInt64Array = 0x119
RegUInt64Array = 0x11A
RegFloatArray = 0x11B
RegDoubleArray = 0x11C
RegUnicodeCharArray = 0x11D
RegBooleanArray = 0x11E
RegUnicodeStringArray = 0x11F

# Constants to support the transaction log files (new format)
LOG_ENTRY_SIZE_HEADER = 40
LOG_ENTRY_SIZE_ALIGNMENT = 0x200

class FileType(Enum):
    FILE_TYPE_PRIMARY = 0
    FILE_TYPE_LOG_OLD_1 = 1 # Starting from Windows XP
    FILE_TYPE_LOG_OLD_2 = 2 # Before Windows XP
    FILE_TYPE_LOG_NEW = 6 # Starting from Windows 8.1

# Added in Windows Vista. Must be applied to Registry type.
# see: http://msdn.microsoft.com/en-us/library/windows/hardware/ff543550%28v=vs.85%29.aspx
DEVPROP_MASK_TYPE = 0x00000FFF

# This named tuple describes the recovery operations to be performed on a hive.
RecoveryStatus = namedtuple('RecoveryStatus', ['recover_header', 'recover_data'])


def parse_timestamp(ticks, resolution, epoch, mode=decimal.ROUND_HALF_EVEN):
    """
    Generalized function for parsing timestamps

    :param ticks: number of time units since the epoch
    :param resolution: number of time units per second
    :param epoch: the datetime of this timestamp's epoch
    :param mode: decimal rounding mode
    :return: datetime.datetime
    """
    # python's datetime.datetime supports microsecond precision
    datetime_resolution = int(1e6)

    # convert ticks since epoch to microseconds since epoch
    us = int((decimal.Decimal(ticks * datetime_resolution) / decimal.Decimal(resolution)).quantize(1, mode))

    # convert to datetime
    return epoch + datetime.timedelta(microseconds=us)


def parse_windows_timestamp(qword):
    """
    :param qword: number of 100-nanoseconds since 1601-01-01
    :return: datetime.datetime
    """
    # see https://msdn.microsoft.com/en-us/library/windows/desktop/ms724290(v=vs.85).aspx
    return parse_timestamp(qword, int(1e7), datetime.datetime(1601, 1, 1))


class RegistryException(Exception):
    """
    Base Exception class for Windows Registry access.
    """

    def __init__(self, value):
        """
        Constructor.
        Arguments:
        - `value`: A string description.
        """
        super(RegistryException, self).__init__()
        self._value = value

    def __str__(self):
        return "Registry Exception: %s" % (self._value)


class RegistryStructureDoesNotExist(RegistryException):
    """
    Exception to be raised when a structure or block is requested which does not exist.
    For example, asking for the ValuesList structure of an NKRecord that has no values
    (and therefore no ValuesList) should result in this exception.
    """
    def __init__(self, value):
        """
        Constructor.
        Arguments:
        - `value`: A string description.
        """
        super(RegistryStructureDoesNotExist, self).__init__(value)

    def __str__(self):
        return "Registry Structure Does Not Exist Exception: %s" % (self._value)


class ParseException(RegistryException):
    """
    An exception to be thrown during Windows Registry parsing, such as
    when an invalid header is encountered.
    """
    def __init__(self, value):
        """
        Constructor.
        Arguments:
        - `value`: A string description.
        """
        super(ParseException, self).__init__(value)

    def __str__(self):
        return "Registry Parse Exception (%s)" % (self._value)


class UnknownTypeException(RegistryException):
    """
    An exception to be raised when an unknown data type is encountered.
    Supported data types current consist of
     - RegSZ
     - RegExpandSZ
     - RegBin
     - RegDWord
     - RegMultiSZ
     - RegQWord
     - RegNone
     - RegBigEndian
     - RegLink
     - RegResourceList
     - RegFullResourceDescriptor
     - RegResourceRequirementsList
     - RegFileTime
    """
    def __init__(self, value):
        """
        Constructor.
        Arguments:
        - `value`: A string description.
        """
        super(UnknownTypeException, self).__init__(value)

    def __str__(self):
        return "Unknown Type Exception (%s)" % (self._value)

class NotSupportedException(RegistryException):
    """
    An exception to be thrown during Windows Registry parsing, when something is not supported yet.
    """
    def __init__(self, value):
        """
        Constructor.
        Arguments:
        - `value`: A string description.
        """
        super(NotSupportedException, self).__init__(value)

    def __str__(self):
        return "Not Supported Exception (%s)" % (self._value)

class RegistryBlock(object):
    """
    Base class for structure blocks in the Windows Registry.
    A block is associated with a offset into a byte-string.

    All blocks (besides the root) also have a parent member, which refers to
    a RegistryBlock that contains a reference to this block, an is found at a
    hierarchically superior rank. Note, by following the parent links upwards,
    the root block should be accessible (aka. there should not be any loops)
    """
    def __init__(self, buf, offset, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block.
        """
        self._buf = buf
        self._offset = offset
        self._parent = parent

    def unpack_binary(self, offset, length):
        return self._buf[self._offset + offset:self._offset + offset + length]

    def unpack_word(self, offset):
        """
        Returns a little-endian WORD (2 bytes) from the relative offset.
        Arguments:
        - `offset`: The relative offset from the start of the block.
        """
        return struct.unpack_from(str("<H"), self._buf, self._offset + offset)[0]

    def unpack_dword(self, offset):
        """
        Returns a little-endian DWORD (4 bytes) from the relative offset.
        Arguments:
        - `offset`: The relative offset from the start of the block.
        """
        return struct.unpack_from(str("<I"), self._buf, self._offset + offset)[0]

    def unpack_int(self, offset):
        """
        Returns a little-endian signed integer (4 bytes) from the relative offset.
        Arguments:
        - `offset`: The relative offset from the start of the block.
        """
        return struct.unpack_from(str("<i"), self._buf, self._offset + offset)[0]

    def unpack_qword(self, offset):
        """
        Returns a little-endian QWORD (8 bytes) from the relative offset.
        Arguments:
        - `offset`: The relative offset from the start of the block.
        """
        return struct.unpack_from(str("<Q"), self._buf, self._offset + offset)[0]

    def unpack_string(self, offset, length):
        """
        Returns a byte string from the relative offset with the given length.
        Arguments:
        - `offset`: The relative offset from the start of the block.
        - `length`: The length of the string.
        """
        return struct.unpack_from(str("<%ds") % (length), self._buf, self._offset + offset)[0]

    def absolute_offset(self, offset):
        """
        Get the absolute offset from an offset relative to this block
        Arguments:
        - `offset`: The relative offset into this block.
        """
        return self._offset + offset

    def parent(self):
        """
        Get the parent block. See the class documentation for what the parent link is.
        """
        return self._parent

    def offset(self):
        """
        Equivalent to self.absolute_offset(0x0), which is the starting offset of this block.
        """
        return self._offset


class REGFBlock(RegistryBlock):
    """
    The Windows Registry file header. This block has a length of 4k, although
    only the first 0x200 bytes are generally used.
    """
    def __init__(self, buf, offset, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block.
        """
        super(REGFBlock, self).__init__(buf, offset, parent)

        _id = self.unpack_dword(0)
        if _id != 0x66676572:
            raise ParseException("Invalid REGF ID")

    def hive_sequence1(self):
        """
        Get first sequence number.
        This is incremented before writing to a primary file.
        """
        return self.unpack_dword(0x4)

    def hive_sequence2(self):
        """
        Get second sequence number.
        This is set to the same value as sequence1 after a primary files has been updated.
        """
        return self.unpack_dword(0x8)

    def validate_sequence_numbers(self):
        """
        Check if sequence numbers are equal.
        """
        return self.hive_sequence1() == self.hive_sequence2()

    def modification_timestamp(self):
        """
        Get the modified timestamp as a Python datetime.
        """
        return parse_windows_timestamp(self.unpack_qword(0xC))

    def reorganized_timestamp(self):
        """
        Get the last reorganized timestamp as a Python datetime.
        The field is used as of Windows 8, the value returned is garbage in previous versions of Windows.
        """
        return parse_windows_timestamp(self.unpack_qword(0xA8))

    def major_version(self):
        """
        Get the major version of the Windows Registry file format
        in use as an unsigned integer.
        """
        return self.unpack_dword(0x14)

    def minor_version(self):
        """
        Get the minor version of the Windows Registry file format
        in use as an unsigned integer.
        """
        return self.unpack_dword(0x18)

    def clustering_factor(self):
        """
        Get the clustering factor.
        """
        return self.unpack_dword(0x2C)

    def file_type(self):
        """
        Get the file type.
        """
        return FileType(self.unpack_dword(0x1C))

    def is_primary_file(self):
        """
        Check if this REGF block belongs to a primary (normal) file.
        """
        return self.file_type() == FileType.FILE_TYPE_PRIMARY

    def is_old_transaction_log_file(self):
        """
        Check if this REGF block belongs to an old transaction log file (used before Windows 8.1).
        """
        return (self.file_type() == FileType.FILE_TYPE_LOG_OLD_1) or (self.file_type() == FileType.FILE_TYPE_LOG_OLD_2)

    def is_new_transaction_log_file(self):
        """
        Check if this REGF block belongs to a new transaction log file (used as of Windows 8.1).
        """
        return self.file_type() == FileType.FILE_TYPE_LOG_NEW

    def file_format(self):
        """
        Get the file format.
        TODO: consider raising an exception if this isn't set to 1 (the only value possible).
        """
        return self.unpack_dword(0x20)

    def hive_flags(self):
        """
        Get the hive flags as an unsigned integer.
        """
        return self.unpack_dword(0x90)

    def hive_name(self):
        """
        Get the hive name of the open Windows Registry file as a string.
        """
        return self.unpack_string(0x30, 64).decode("utf-16le").rstrip("\x00")

    def first_hbin_offset(self):
        """
        Get the buffer offset of the first HBINBlock as an unsigned integer.
        Note: always returns 0x1000, nothing else is possible.
        """
        return 0x1000

    def hbins_size(self):
        """
        Size of all HBINBlock structures as an unsigned integer.
        """
        return self.unpack_dword(0x28)

    def last_hbin_offset(self):
        """
        Obsolete, use hbins_size instead.
        This doesn't return the offset of the last HBINBlock (as was believed before).
        """
        from warnings import warn
        warn("last_hbin_offset is obsolete, use hbins_size instead!")
        return self.unpack_dword(0x28)

    def calculate_checksum(self):
        """
        Checksum is calculated over the first 0x200 bytes:
        XOR of all D-Words from 0x00000000 to 0x000001FB with two edge cases.
        """
        xsum = 0
        idx = 0x0
        while idx <= 0x1FB:
            xsum ^= self.unpack_dword(idx)
            idx += 0x4
        if xsum == 0:
            return 1
        if xsum == 0xFFFFFFFF:
            return 0xFFFFFFFE
        return xsum

    def checksum(self):
        """
        Get the checksum stored in hive.
        """
        return self.unpack_dword(0x1FC)

    def validate_checksum(self):
        """
        Is the file checksum valid?
        """
        return self.calculate_checksum() == self.checksum()

    def validate(self):
        """
        Are the file checksum and sequence numbers valid?
        Obsolete, use recovery_required instead.
        """
        from warnings import warn
        warn("validate is obsolete, use recovery_required instead!")
        return self.validate_checksum() and self.validate_sequence_numbers()

    def recovery_required(self):
        """
        Are the file checksum and sequence numbers valid?
        Return a named tuple with two boolean values:
          - the recover_header is True when the REGF block recovery is required,
          - the recover_data is True when data recovery is required.
        """
        if not self.validate_checksum():
            # Header is invalid, this also implies data recovery
            return RecoveryStatus(recover_header = True, recover_data = True)

        if not self.validate_sequence_numbers():
            # Header is valid, data is in the mid-update state
            return RecoveryStatus(recover_header = False, recover_data = True)

        return RecoveryStatus(recover_header = False, recover_data = False)

    def first_key(self):
        first_hbin = next(self.hbins())

        key_offset = first_hbin.absolute_offset(self.unpack_dword(0x24))

        d = HBINCell(self._buf, key_offset, first_hbin)
        return NKRecord(self._buf, d.data_offset(), first_hbin)

    def hbins(self):
        """
        A generator that enumerates all HBIN (HBINBlock) structures in this Windows Registry.
        """
        h = HBINBlock(self._buf, self.first_hbin_offset(), self)
        yield h

        while h.has_next():
            h = h.next()
            yield h

    def first_log_entry_offset(self):
        """
        Get the offset of the first log entry as an unsigned integer.
        Note: always returns 0x200, nothing else is possible in new transaction log files.
        """
        return 0x200

    def log_entries(self):
        """
        A generator that enumerates all valid HvLE (HvLEBlock) structures in the transaction log file.
        """
        expected_seqnum = c_uint32(self.hive_sequence2())
        h = HvLEBlock(self._buf, self.first_log_entry_offset(), self)
        if h.sequence() == expected_seqnum.value and h.validate_log_entry():
            yield h

            while h.has_next():
                h = h.next()
                expected_seqnum.value += 1
                if h.sequence() == expected_seqnum.value and h.validate_log_entry():
                    yield h
                else:
                    break


class HBINCell(RegistryBlock):
    """
    HBIN data cell. An HBINBlock is continuously filled with HBINCell structures.
    The general structure is the length of the block, followed by a blob of data.
    """
    def __init__(self, buf, offset, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block.
        """
        super(HBINCell, self).__init__(buf, offset, parent)
        self._size = self.unpack_int(0x0)

    def __str__(self):
        if self.is_free():
            return "HBIN Cell (free) at 0x%x" % (self._offset)
        else:
            return "HBIN Cell at 0x%x" % (self._offset)

    def is_free(self):
        """
        Is the cell free?
        """
        return self._size > 0

    def size(self):
        """
        Size of this cell, as an unsigned integer.
        """
        if self.is_free():
            return self._size
        else:
            return self._size * -1

    def next(self):
        """
        Returns the next HBINCell, which is located immediately after this.
        Note: This will always return an HBINCell starting at the next location
        whether or not the buffer is large enough. The calling function should
        check the offset of the next HBINCell to ensure it does not overrun the
        HBIN buffer.
        """
        try:
            return HBINCell(self._buf, self._offset + self.size(), self.parent())
        except:
            raise RegistryStructureDoesNotExist("HBINCell does not exist at 0x%x" % (self._offset + self.size()))

    def offset(self):
        """
        Accessor for absolute offset of this HBINCell.
        """
        return self._offset

    def data_offset(self):
        """
        Get the absolute offset of the data block of this HBINCell.
        """
        return self._offset + 0x4

    def raw_data(self):
        """
        Get the raw data from the buffer contained by this HBINCell.
        """
        return self._buf[self.data_offset():self.data_offset() + self.size()]

    def data_id(self):
        """
        Get the ID string of the data block of this HBINCell.
        """
        return self.unpack_string(0x4, 2)

    def abs_offset_from_hbin_offset(self, offset):
        """
        Offsets contained in HBIN cells are relative to the beginning of the first HBIN.
        This converts the relative offset into an absolute offset.
        """
        h = self.parent()
        while h.__class__.__name__ != "HBINBlock":
            h = h.parent()

        return h.first_hbin().offset() + offset

    def child(self):
        """
        Make a _guess_ as to the contents of this structure and
        return an instance of that class, or just a DataRecord
        otherwise.
        """
        if self.is_free():
            raise RegistryStructureDoesNotExist("HBINCell is free at 0x%x" % (self.offset()))

        id_ = self.data_id()

        if id_ == b"vk":
            return VKRecord(self._buf, self.data_offset(), self)
        elif id_ == b"nk":
            return NKRecord(self._buf, self.data_offset(), self)
        elif id_ == b"lf":
            return LFRecord(self._buf, self.data_offset(), self)
        elif id_ == b"lh":
            return LHRecord(self._buf, self.data_offset(), self)
        elif id_ == b"li":
            return LIRecord(self._buf, self.data_offset(), self)
        elif id_ == b"ri":
            return RIRecord(self._buf, self.data_offset(), self)
        elif id_ == b"sk":
            return SKRecord(self._buf, self.data_offset(), self)
        elif id_ == b"db":
            return DBRecord(self._buf, self.data_offset(), self)
        else:
            return DataRecord(self._buf, self.data_offset(), self)


class Record(RegistryBlock):
    """
    Abstract class for Records contained by cells in HBINs
    """
    def __init__(self, buf, offset, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block. This SHOULD be an HBINCell.
        """
        super(Record, self).__init__(buf, offset, parent)

    def abs_offset_from_hbin_offset(self, offset):
        # TODO This violates DRY as this is a redefinition, see HBINCell.abs_offset_from_hbin_offset()
        """
        Offsets contained in HBIN cells are relative to the beginning of the first HBIN.
        This converts the relative offset into an absolute offset.
        """
        h = self.parent()
        while h.__class__.__name__ != "HBINBlock":
            h = h.parent()

        return h.first_hbin().offset() + offset


class DataRecord(Record):
    """
    A DataRecord is a HBINCell that does not contain any further structural data, but
    may contain, for example, the values pointed to by a VKRecord.
    """
    def __init__(self, buf, offset, parent):
        """
        Constructor.

        Arguments:
        - `buf`: Byte string containing Windows Registry file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block. This should be an HBINCell.
        """
        super(DataRecord, self).__init__(buf, offset, parent)

    def __str__(self):
        return "Data Record at 0x%x" % (self.offset())


class DBIndirectBlock(Record):
    """
    The DBIndirect block is a list of offsets to DataRecords with data
    size up to 0x3fd8.
    """
    def __init__(self, buf, offset, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block. This should be an HBINCell.
        """
        super(DBIndirectBlock, self).__init__(buf, offset, parent)

    def __str__(self):
        return "Large Data Block at 0x%x" % (self.offset())

    def large_data(self, length):
        """
        Get the data pointed to by the indirect block. It may be large.
        Return a byte string.
        """
        b = bytearray()
        count = 0
        while length > 0:
            off = self.abs_offset_from_hbin_offset(self.unpack_dword(4 * count))
            size = min(0x3fd8, length)
            b += HBINCell(self._buf, off, self).raw_data()[0:size]

            count += 1
            length -= size
        return bytes(b)


class DBRecord(Record):
    """
    A DBRecord is a large data block, which is not thoroughly documented.
    Its similar to an inode in the Ext file systems.
    """
    def __init__(self, buf, offset, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block. This should be an HBINCell.
        """
        super(DBRecord, self).__init__(buf, offset, parent)

        _id = self.unpack_string(0x0, 2)
        if _id != b"db":
            raise ParseException("Invalid DB Record ID")

    def __str__(self):
        return "Large Data Block at 0x%x" % (self.offset())

    def large_data(self, length):
        """
        Get the data described by the DBRecord. It may be large.
        Return a byte array.
        """
        off = self.abs_offset_from_hbin_offset(self.unpack_dword(0x4))
        cell = HBINCell(self._buf, off, self)
        dbi = DBIndirectBlock(self._buf, cell.data_offset(), cell)
        return dbi.large_data(length)


def decode_utf16le(s):
    """
    decode_utf16le attempts to decode a bytestring as UTF-16LE.
      If the string has an odd length, or some unexpected feature,
      this function does its best to handle the data. It does not
      catch any Unicode-related exceptions, such as UnicodeDecodeError,
      so these should be handled by the caller.

    @type s: bytes
    @param s: a bytestring to pase
    @rtype: unicode
    @return: the unicode string decoded from `s`
    @raises: this function does not attempt to catch any Unicode-related exception, so the caller should handle these.
    """
    if b"\x00\x00" in s:
        index = s.index(b"\x00\x00")
        if index > 2:
            if s[index - 2] != b"\x00"[0]: #py2+3
                #  61 00 62 00 63 64 00 00
                #                    ^  ^-- end of string
                #                    +-- index
                s = s[:index + 2]
            else:
                #  61 00 62 00 63 00 00 00
                #                 ^     ^-- end of string
                #                 +-- index
                s = s[:index + 3]
    if (len(s) % 2) != 0:
        s = s + b"\x00"
    s = s.decode("utf16")
    s = s.partition('\x00')[0]
    return s


class VKRecord(Record):
    """
    The VKRecord holds one name-value pair.  The data may be one of many types,
    including strings, integers, and binary data.
    """
    def __init__(self, buf, offset, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block.
              This should be an HBINCell.
        """
        super(VKRecord, self).__init__(buf, offset, parent)

        _id = self.unpack_string(0x0, 2)
        if _id != b"vk":
            raise ParseException("Invalid VK Record ID")

    def data_type_str(self):
        """
        Get the value data's type as a string
        """
        data_type = self.data_type()
        if data_type == RegSZ:
            return "RegSZ"
        elif data_type == RegExpandSZ:
            return "RegExpandSZ"
        elif data_type == RegBin:
            return "RegBin"
        elif data_type == RegDWord:
            return "RegDWord"
        elif data_type == RegMultiSZ:
            return "RegMultiSZ"
        elif data_type == RegQWord:
            return "RegQWord"
        elif data_type == RegNone:
            return "RegNone"
        elif data_type == RegBigEndian:
            return "RegBigEndian"
        elif data_type == RegLink:
            return "RegLink"
        elif data_type == RegResourceList:
            return "RegResourceList"
        elif data_type == RegFullResourceDescriptor:
            return "RegFullResourceDescriptor"
        elif data_type == RegResourceRequirementsList:
            return "RegResourceRequirementsList"
        elif data_type == RegFileTime:
            return "RegFileTime"
        elif data_type == RegUint8:
            return "RegUint8"
        elif data_type == RegInt16:
            return "RegInt16"
        elif data_type == RegUint16:
            return "RegUint16"
        elif data_type == RegInt32:
            return "RegInt32"
        elif data_type == RegUint32:
            return "RegUint32"
        elif data_type == RegInt64:
            return "RegInt64"
        elif data_type == RegUint64:
            return "RegUint64"
        elif data_type == RegFloat:
            return "RegFloat"
        elif data_type == RegDouble:
            return "RegDouble"
        elif data_type == RegUnicodeChar:
            return "RegUnicodeChar"
        elif data_type == RegBoolean:
            return "RegBoolean"
        elif data_type == RegUnicodeString:
            return "RegUnicodeString"
        elif data_type == RegCompositeValue:
            return "RegCompositeValue"
        elif data_type == RegDateTimeOffset:
            return "RegDateTimeOffset"
        elif data_type == RegTimeSpan:
            return "RegTimeSpan"
        elif data_type == RegGUID:
            return "RegGUID"
        elif data_type == RegUnk111:
            return "RegUnk111"
        elif data_type == RegUnk112:
            return "RegUnk112"
        elif data_type == RegUnk113:
            return "RegUnk113"
        elif data_type == RegBytesArray:
            return "RegBytesArray"
        elif data_type == RegInt16Array:
            return "RegInt16Array"
        elif data_type == RegUint16Array:
            return "RegUint16Array"
        elif data_type == RegInt32Array:
            return "RegInt32Array"
        elif data_type == RegUInt32Array:
            return "RegUInt32Array"
        elif data_type == RegInt64Array:
            return "RegInt64Array"
        elif data_type == RegUInt64Array:
            return "RegUInt64Array"
        elif data_type == RegFloatArray:
            return "RegFloatArray"
        elif data_type == RegDoubleArray:
            return "RegDoubleArray"
        elif data_type == RegUnicodeCharArray:
            return "RegUnicodeCharArray"
        elif data_type == RegBooleanArray:
            return "RegBooleanArray"
        elif data_type == RegUnicodeStringArray:
            return "RegUnicodeStringArray"
        else:
            return "Unknown type: %s" % (hex(data_type))

    def __str__(self):
        if self.has_name():
            name = self.name()
        else:
            name = "(default)"

        data = ""
        data_type = self.data_type()
        if data_type == RegSZ or data_type == RegExpandSZ:
            data = self.data()[0:16] + "..."
        elif data_type == RegMultiSZ:
            data = str(len(self.data())) + " strings"
        elif data_type == RegDWord or data_type == RegQWord:
            data = str(hex(self.data()))
        elif data_type == RegNone:
            data = "(none)"
        elif data_type == RegBin:
            data = "(binary)"
        elif data_type in (RegFileTime, RegDateTimeOffset):
            data = self.data().isoformat("T") + "Z"
        elif data_type in (RegUint8, RegInt16, RegUint16, RegInt32, RegUint32,
                              RegInt64, RegUint64, RegFloat, RegDouble, RegUnicodeChar,
                              RegBoolean, RegUnicodeString, RegCompositeValue,
                              RegTimeSpan, RegGUID, RegUnk111, RegUnk112, RegUnk113, RegBytesArray,
                              RegInt16Array, RegUint16Array, RegInt32Array, RegUInt32Array,
                              RegInt64Array, RegUInt64Array, RegFloatArray, RegDoubleArray,
                              RegUnicodeCharArray, RegBooleanArray, RegUnicodeStringArray):
            data = str(self.data())
        else:
            data = "(unsupported)"

        return "VKRecord(Name: %s, Type: %s, Data: %s) at 0x%x" % (name,
                                                         self.data_type_str(),
                                                         data,
                                                         self.offset())

    def has_name(self):
        """
        Has a name? or perhaps we should use '(default)'
        """
        return self.unpack_word(0x2) != 0

    def has_ascii_name(self):
        """
        Is the name of this value in the ASCII charset?
        """
        return self.unpack_word(0x10) & 1 == 1

    def name(self):
        """
        Get the name, if it exists. If not, the empty string is returned.
        @return: unicode string containing the name
        """
        if not self.has_name():
            return ""
        name_length = self.unpack_word(0x2)
        unpacked_string = self.unpack_string(0x14, name_length)
        if self.has_ascii_name():
            return unpacked_string.decode("windows-1252")
        return unpacked_string.decode("utf-16le")

    def has_timestamp(self):
        """
        Has a timestamp? Only AppContainer settings.dat registry hive has this!
        """
        return (self.data_type() & 0x100 == 0x100) and (self.raw_data_length() >= 8)

    def timestamp(self):
        """
        Get the modified timestamp as a Python datetime. This is only valid for
        AppContainer settings.dat registry hive
        """
        if self.has_timestamp():
            return parse_windows_timestamp(struct.unpack_from(str("<Q"), self.raw_data()[-8:])[0])
        raise ValueError('value does not have a timestamp')

    def data_type(self):
        """
        Get the data type of this value data as an unsigned integer.
        """
        return self.unpack_dword(0xC) & DEVPROP_MASK_TYPE

    def data_length(self):
        """
        Get the length of this value data. This is the actual length of the data that should be parsed for the value.
        """
        size = self.unpack_dword(0x4)
        if size >= 0x80000000:
            size -= 0x80000000
        return size

    def raw_data_length(self):
        """
        Get the literal length of this value data. Some interpretation may be required to make sense of the value.
        """
        return self.unpack_dword(0x4)

    def data_offset(self):
        """
        Get the offset to the raw data associated with this value.
        """
        if self.raw_data_length() < 5 or self.raw_data_length() >= 0x80000000:
            return self.absolute_offset(0x8)
        else:
            return self.abs_offset_from_hbin_offset(self.unpack_dword(0x8))

    def raw_data(self, overrun=0):
        """
        Get the unparsed raw data.
        """
        data_type = self.data_type()
        data_length = self.raw_data_length()
        data_offset = self.data_offset()
        ret = None

        if data_type == RegSZ or data_type == RegExpandSZ:
            if data_length >= 0x80000000:
                # data is contained in the data_offset field
                ret = self._buf[data_offset:data_offset + 0x4]
            elif 0x3fd8 < data_length < 0x80000000:
                d = HBINCell(self._buf, data_offset, self)
                if d.data_id() == b"db":
                    # this should always be the case
                    # but empirical testing does not confirm this
                    ret = d.child().large_data(data_length + overrun)
                else:
                    ret = d.raw_data()[:data_length + overrun]
            else:
                d = HBINCell(self._buf, data_offset, self)
                data_offset = d.data_offset()
                ret = self._buf[data_offset:data_offset + data_length]
        elif data_type == RegBin or data_type == RegNone \
             or data_type in (RegUint8, RegInt16, RegUint16, RegInt32, RegUint32, 
                              RegInt64, RegUint64, RegFloat, RegDouble, RegUnicodeChar, 
                              RegBoolean, RegUnicodeString, RegCompositeValue,RegDateTimeOffset, 
                              RegTimeSpan, RegGUID, RegUnk111, RegUnk112, RegUnk113, RegBytesArray, 
                              RegInt16Array, RegUint16Array, RegInt32Array, RegUInt32Array, 
                              RegInt64Array, RegUInt64Array, RegFloatArray, RegDoubleArray, 
                              RegUnicodeCharArray, RegBooleanArray, RegUnicodeStringArray):
            if data_length >= 0x80000000:
                data_length -= 0x80000000
                ret = self._buf[data_offset:data_offset + data_length + overrun]
            elif 0x3fd8 < data_length < 0x80000000:
                d = HBINCell(self._buf, data_offset, self)
                if d.data_id() == b"db":
                    # this should always be the case
                    # but empirical testing does not confirm this
                    ret = d.child().large_data(data_length + overrun)
                else:
                    ret = d.raw_data()[:data_length + overrun]
            else:
                ret = self._buf[data_offset + 4:data_offset + 4 + data_length + overrun]
        elif data_type == RegDWord:
            ret = self.unpack_binary(0x8, 0x4)
        elif data_type == RegMultiSZ:
            if data_length >= 0x80000000:
                # this means data_length < 5, so it must be 4, and
                # be composed of completely \x00, so the strings are empty
                ret = b""
            elif 0x3fd8 < data_length < 0x80000000:
                d = HBINCell(self._buf, data_offset, self)
                if d.data_id() == b"db":
                    ret = d.child().large_data(data_length + overrun)
                else:
                    ret = d.raw_data()[:data_length + overrun]
            else:
                ret = self._buf[data_offset + 4:data_offset + 4 + data_length + overrun]
        elif data_type == RegQWord:
            d = HBINCell(self._buf, data_offset, self)
            data_offset = d.data_offset()
            ret = self._buf[data_offset:data_offset + 0x8]
        elif data_type == RegBigEndian:
            d = HBINCell(self._buf, data_offset, self)
            data_offset = d.data_offset()
            ret = self._buf[data_offset:data_offset + 4]
        elif data_type == RegLink or \
                        data_type == RegResourceList or \
                        data_type == RegFullResourceDescriptor or \
                        data_type == RegResourceRequirementsList:
            if data_length >= 0x80000000:
                data_length -= 0x80000000
                ret = self._buf[data_offset:data_offset + data_length]
            elif 0x3fd8 < data_length < 0x80000000:
                d = HBINCell(self._buf, data_offset, self)
                if d.data_id() == b"db":
                    # this should always be the case
                    # but empirical testing does not confirm this
                    ret = d.child().large_data(data_length)
                else:
                    ret = d.raw_data()[:data_length]
            else:
                ret = self._buf[data_offset + 4:data_offset + 4 + data_length]
        elif data_type == RegFileTime:
            ret = self._buf[data_offset + 4:data_offset + 4 + data_length]
        elif data_length < 5 or data_length >= 0x80000000:
            ret = self.unpack_binary(0x8, 4)
        else:
            if data_length >= 0x80000000:
                data_length -= 0x80000000
                ret = self._buf[data_offset:data_offset + data_length]
            elif 0x3fd8 < data_length < 0x80000000:
                d = HBINCell(self._buf, data_offset, self)
                if d.data_id() == b"db":
                    # this should always be the case
                    # but empirical testing does not confirm this
                    ret = d.child().large_data(data_length)
                else:
                    ret = d.raw_data()[:data_length]
            else:
                ret = self._buf[data_offset + 4:data_offset + 4 + data_length]
        return ret

    def data(self, overrun=0):
        """
        Get the parsed data.
        This method will return various types based on the data type.

        RegSZ:
          Return a string containing the data, doing the best we can to convert it
          to ASCII or UNICODE.
        RegExpandSZ:
          Return a string containing the data, doing the best we can to convert it
          to ASCII or UNICODE. The special variables are not expanded.
        RegMultiSZ:
          Return a list of strings.
        RegNone:
          See RegBin
        RegDword:
          Return an unsigned integer containing the data.
        RegQword:
          Return an unsigned integer containing the data.
        RegBin:
          Return a sequence of bytes containing the binary data.
        RegBigEndian:
          Not currently supported. TODO.
        RegLink:
          Not currently supported. TODO.
        RegResourceList:
          Not currently supported. TODO.
        RegFullResourceDescriptor:
          Not currently supported. TODO.
        RegResourceRequirementsList:
          Not currently supported. TODO.
        RegFileTime:
          Return a datetime.datetime object
        """
        data_type = self.data_type()
        data_length = self.raw_data_length()
        d = self.raw_data(overrun=overrun)

        if data_type == RegSZ or data_type == RegExpandSZ:
            if overrun > 0:
                # decode_utf16le() only returns the first string, but if we explicitly
                # ask for overrun, let's make a best-effort to decode as much as possible.
                return d.decode('utf16')
            else:
                return decode_utf16le(d)
        elif data_type == RegBin or data_type == RegNone:
            return d
        elif data_type == RegDWord:
            return struct.unpack_from(str("<I"), d, 0)[0]
        elif data_type == RegMultiSZ:
            s = d.decode("utf16")
            return s.split("\x00")
        elif data_type == RegQWord:
            return struct.unpack_from(str("<Q"), d, 0)[0]
        elif data_type == RegBigEndian:
            return struct.unpack_from(str(">I"), d, 0)[0]
        elif data_type == RegLink or \
                        data_type == RegResourceList or \
                        data_type == RegFullResourceDescriptor or \
                        data_type == RegResourceRequirementsList:
            # we don't really support these types, but can at least
            #  return raw binary for someone else to work with.
            return d
        elif data_type in (RegUint8, RegInt16, RegUint16, RegInt32, RegUint32, 
                        RegInt64, RegUint64, RegFloat, RegDouble, RegUnicodeChar, 
                        RegBoolean, RegUnicodeString, RegCompositeValue,RegDateTimeOffset, 
                        RegTimeSpan, RegGUID, RegUnk111, RegUnk112, RegUnk113, RegBytesArray, 
                        RegInt16Array, RegUint16Array, RegInt32Array, RegUInt32Array, 
                        RegInt64Array, RegUInt64Array, RegFloatArray, RegDoubleArray, 
                        RegUnicodeCharArray, RegBooleanArray, RegUnicodeStringArray):
            d = d[0:-8] # remove timestamp from end
            comp_type = data_type & 0xEFF # Apply mask for composite types
            return SettingsParse.ParseAppDataCompositeValue(comp_type, d, len(d))
        elif data_type == RegFileTime:
            return parse_windows_timestamp(struct.unpack_from(str("<Q"), d, 0)[0])
        elif data_length < 5 or data_length >= 0x80000000:
            return struct.unpack_from(str("<I"), d, 0)[0]
        else:
            raise UnknownTypeException("Unknown VK Record type 0x%x at 0x%x" % (data_type, self.offset()))


class SKRecord(Record):
    """
    Security Record. Contains Windows security descriptor,
    Which defines ownership and permissions for local values
    and subkeys.

    May be referenced by multiple NK records.
    """
    def __init__(self, buf, offset, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block. This should be an HBINCell.
        """
        super(SKRecord, self).__init__(buf, offset, parent)

        _id = self.unpack_string(0x0, 2)
        if _id != b"sk":
            raise ParseException("Invalid SK Record ID")

        self._offset_prev_sk = self.unpack_dword(0x4)
        self._offset_next_sk = self.unpack_dword(0x8)

    def __str__(self):
        return "SK Record at 0x%x" % (self.offset())


class ValuesList(HBINCell):
    """
    A ValuesList is a simple structure of fixed length pointers/offsets to VKRecords.
    """
    def __init__(self, buf, offset, parent, number):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block. The parent of a ValuesList SHOULD be a NKRecord.
        """
        super(ValuesList, self).__init__(buf, offset, parent)
        self._number = number

    def __str__(self):
        return "ValueList(Length: %d) at 0x%x" % (self.parent().values_number(), self.offset())

    def values(self):
        """
        A generator that yields the VKRecords referenced by this list.
        """
        value_item = 0x0

        for _ in range(0, self._number):
            value_offset = self.abs_offset_from_hbin_offset(self.unpack_dword(value_item))

            d = HBINCell(self._buf, value_offset, self)
            v = VKRecord(self._buf, d.data_offset(), self)
            value_item += 4
            yield v


class SubkeyList(Record):
    """
    A base class for use by structures recording the subkeys of Registry key.
    The required overload is self.keys(), which is a generator for all the subkeys (NKRecords).
    The SubkeyList is not meant to be used directly.
    """
    def __init__(self, buf, offset, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block. The parent of a SubkeyList SHOULD be a NKRecord.
        """
        super(SubkeyList, self).__init__(buf, offset, parent)

    def __str__(self):
        return "SubkeyList(Length: %d) at 0x%x" % (0, self.offset())

    def _keys_len(self):
        return self.unpack_word(0x2)

    def keys(self):
        """
        A generator that yields the NKRecords referenced by this list.
        The base SubkeyList class returns no NKRecords, since it should not be used directly.
        """
        return


class RIRecord(SubkeyList):
    """
    The RIRecord is a structure linking to structures containing
    a lists of offsets/pointers to subkey NKRecords. It is like a double (or more)
    indirect block.
    """
    def __init__(self, buf, offset, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block.
        """
        super(RIRecord, self).__init__(buf, offset, parent)

    def __str__(self):
        return "RIRecord(Length: %d) at 0x%x" % (len(self.keys()), self.offset())

    def keys(self):
        """
        A generator that yields the NKRecords referenced by this list.
        ri style entry size.
        """
        key_index = 0x4

        for _ in range(0, self._keys_len()):
            key_offset = self.abs_offset_from_hbin_offset(self.unpack_dword(key_index))
            d = HBINCell(self._buf, key_offset, self)

            try:
                for k in d.child().keys():
                    yield k
            except RegistryStructureDoesNotExist:
                raise ParseException("Unsupported subkey list encountered.")

            key_index += 4


class DirectSubkeyList(SubkeyList):
    def __init__(self, buf, offset, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block.
        """
        super(DirectSubkeyList, self).__init__(buf, offset, parent)

    def __str__(self):
        return "DirectSubkeyList(Length: %d) at 0x%x" % (self._keys_len(), self.offset())

    def keys(self):
        """
        A generator that yields the NKRecords referenced by this list.
        Assumes each entry is 0x8 bytes long (lf / lh style).
        """
        key_index = 0x4

        for _ in range(0, self._keys_len()):
            key_offset = self.abs_offset_from_hbin_offset(self.unpack_dword(key_index))

            d = HBINCell(self._buf, key_offset, self)
            yield NKRecord(self._buf, d.data_offset(), self)
            key_index += 8


class LIRecord(DirectSubkeyList):
    """
    The LIRecord is a simple structure containing a list of offsets/pointers
    to subkey NKRecords. It is a single indirect block.
    """
    def __init__(self, buf, offset, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block.
        """
        super(LIRecord, self).__init__(buf, offset, parent)

    def __str__(self):
        return "LIRecord(Length: %d) at 0x%x" % (self._keys_len(), self.offset())

    def keys(self):
        """
        A generator that yields the NKRecords referenced by this list.
        li style entry size.
        """
        key_index = 0x4

        for _ in range(0, self._keys_len()):
            key_offset = self.abs_offset_from_hbin_offset(self.unpack_dword(key_index))

            d = HBINCell(self._buf, key_offset, self)
            yield NKRecord(self._buf, d.data_offset(), self)
            key_index += 4


class LFRecord(DirectSubkeyList):
    """
    The LFRecord is a simple structure containing a list of offsets/pointers
    to subkey NKRecords.
    The LFRecord also contains a hash for the name of the subkey pointed to
    by the offset, which enables more efficient searching of the Registry tree.
    """
    def __init__(self, buf, offset, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block.
        """
        super(LFRecord, self).__init__(buf, offset, parent)
        _id = self.unpack_string(0x0, 2)
        if _id != b"lf":
            raise ParseException("Invalid LF Record ID")

    def __str__(self):
        return "LFRecord(Length: %d) at 0x%x" % (self._keys_len(), self.offset())


class LHRecord(DirectSubkeyList):
    """
    The LHRecord is a simple structure containing a list of offsets/pointers
    to subkey NKRecords.
    The LHRecord also contains a hash for the name of the subkey pointed to
    by the offset, which enables more efficient searching of the Registry tree.
    The LHRecord is analogous to the LFRecord, but it uses a different hashing function.
    """
    def __init__(self, buf, offset, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block.
        """
        super(LHRecord, self).__init__(buf, offset, parent)
        _id = self.unpack_string(0x0, 2)
        if _id != b"lh":
            raise ParseException("Invalid LH Record ID")

    def __str__(self):
        return "LHRecord(Length: %d) at 0x%x" % (self._keys_len(), self.offset())


class NKRecord(Record):
    """
    The NKRecord defines the tree-like structure of the Windows Registry.
    It contains pointers/offsets to the ValueList (values associated with the given record),
    and to subkeys.
    """
    def __init__(self, buf, offset, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block. This should be a HBINCell.
        """
        super(NKRecord, self).__init__(buf, offset, parent)
        _id = self.unpack_string(0x0, 2)
        if _id != b"nk":
            raise ParseException("Invalid NK Record ID")

    def __str__(self):
        classname = self.classname()
        if not self.has_classname():
            classname = "(none)"

        if self.is_root():
            return "Root NKRecord(Class: %s, Name: %s) at 0x%x" % (classname,
                                                                   self.name(),
                                                                   self.offset())
        else:
            return "NKRecord(Class: %s, Name: %s) at 0x%x" % (classname,
                                                              self.name(),
                                                              self.offset())

    def has_classname(self):
        """
        Does this have a classname?
        """
        return self.unpack_word(0x4A) > 0

    def classname(self):
        """
        If this has a classname, get it as a string. Otherwise, return the empty string.
        @return: unicode string containing the class name
        """
        if not self.has_classname():
            return ""

        classname_offset = self.unpack_dword(0x30)
        classname_length = self.unpack_word(0x4A)

        offset = self.abs_offset_from_hbin_offset(classname_offset)
        d = HBINCell(self._buf, offset, self)
        return struct.unpack_from(str("<%ds") % (classname_length), self._buf, d.data_offset())[0].decode("utf-16le").rstrip("\x00")

    def timestamp(self):
        """
        Get the modified timestamp as a Python datetime.
        """
        return parse_windows_timestamp(self.unpack_qword(0x4))

    def access_bits(self):
        """
        Get the access bits of the registry key as an unsigned integer.
        The field is used as of Windows 8.
        """
        return self.unpack_dword(0xC) & 0xFF

    def has_ascii_name(self):
        return self.unpack_word(0x2) & 0x0020 > 0

    def name(self):
        """
        Return the registry key name as a string.
        @return: unicode string containing the name
        """
        name_length = self.unpack_word(0x48)
        unpacked_string = self.unpack_string(0x4C, name_length)
        if self.has_ascii_name():
            return unpacked_string.decode("windows-1252")
        return unpacked_string.decode("utf-16le")

    def path(self):
          """
          Return the full path of the registry key as a unicode string
          @return: unicode string containing the path
          """
          p = self

          name = [p.name()]
          offsets = set([p._offset])
          while p.has_parent_key():
              p = p.parent_key()
              if p._offset in offsets:
                  name.append("[path cycle]")
                  break
              name.append(p.name())
              offsets.add(p._offset)
          return '\\'.join(reversed(name))

    def is_root(self):
        """
        Is this a root key?
        """
        return self.unpack_word(0x2) & 0x0004 > 0

    def has_parent_key(self):
        """
        Is there a parent key? There should always be a parent key, unless
        this is a root key (see self.is_root())
        """
        if self.is_root():
            return False
        try:
            self.parent_key()
            return True
        except ParseException:
            return False

    def parent_key(self):
        """
        Get the parent_key, which will be an NKRecord.
        """
        offset = self.abs_offset_from_hbin_offset(self.unpack_dword(0x10))

        d = HBINCell(self._buf, offset, self.parent())
        return NKRecord(self._buf, d.data_offset(), self.parent())

    def sk_record(self):
        """
        Get the security descriptor associated with this NKRecord as an SKRecord.
        """
        offset = self.abs_offset_from_hbin_offset(self.unpack_dword(0x2C))

        d = HBINCell(self._buf, offset, self)
        return SKRecord(self._buf, d.data_offset(), d)

    def values_number(self):
        """
        Get the number of values associated with this NKRecord/Key.
        """
        num = self.unpack_dword(0x24)
        if num == 0xFFFFFFFF:
            return 0
        return num

    def values_list(self):
        """
        Get the values as a ValuesList.
        Raises RegistryStructureDoesNotExist if this NKRecord has no values.
        """
        if self.values_number() == 0:
            raise RegistryStructureDoesNotExist("NK Record has no associated values.")

        values_list_offset = self.abs_offset_from_hbin_offset(self.unpack_dword(0x28))

        d = HBINCell(self._buf, values_list_offset, self)
        return ValuesList(self._buf, d.data_offset(), self, self.values_number())

    def subkey_number(self):
        """
        Get the number of subkeys of this key.
        """
        number = self.unpack_dword(0x14)
        if number == 0xFFFFFFFF:
            return 0
        return number

    def subkey_list(self):
        """
        Get the subkeys of this key as a descendant of SubkeyList.
        Raises RegistryStructureDoesNotExists if this NKRecord does not have any subkeys.
        See NKRecord.subkey_number() to check for the existence of subkeys.
        """
        if self.subkey_number() == 0:
            raise RegistryStructureDoesNotExist("NKRecord has no subkey list at 0x%x" % (self.offset()))

        subkey_list_offset = self.abs_offset_from_hbin_offset(self.unpack_dword(0x1C))

        d = HBINCell(self._buf, subkey_list_offset, self)
        id_ = d.data_id()

        if id_ == b"lf":
            l = LFRecord(self._buf, d.data_offset(), self)
        elif id_ == b"lh":
            l = LHRecord(self._buf, d.data_offset(), self)
        elif id_ == b"ri":
            l = RIRecord(self._buf, d.data_offset(), self)
        elif id_ == b"li":
            l = LIRecord(self._buf, d.data_offset(), self)
        else:
            raise ParseException("Subkey list with type 0x%s encountered, but not yet supported." %
                                 (binascii.hexlify(id_).decode('ascii')))

        return l


class HBINBlock(RegistryBlock):
    """
    A HBINBlock is the basic allocation block of the Windows Registry.
    It's length is multiple of 0x1000.
    """
    def __init__(self, buf, offset, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block. The parent of the first HBINBlock
        should be the REGFBlock, and the parents of other HBINBlocks should be the preceding
        HBINBlocks.
        """
        super(HBINBlock, self).__init__(buf, offset, parent)

        _id = self.unpack_dword(0)
        if _id != 0x6E696268:
            raise ParseException("Invalid HBIN ID")

        self._reloffset_next_hbin = self.unpack_dword(0x8)
        self._offset_next_hbin = self._reloffset_next_hbin + self._offset

    def __str__(self):
        return "HBIN at 0x%x" % (self._offset)

    def first_hbin(self):
        """
        Get the first HBINBlock.
        """
        reloffset_from_first_hbin = self.unpack_dword(0x4)
        return HBINBlock(self._buf, (self.offset() - reloffset_from_first_hbin), self.parent())

    def has_next(self):
        """
        Does another HBINBlock exist after this one?
        """
        regf = self.first_hbin().parent()
        if regf.hbins_size() + regf.first_hbin_offset() == self._offset_next_hbin:
            return False

        try:
            self.next()
            return True
        except (ParseException, struct.error):
            return False

    def next(self):
        """
        Get the next HBIN after this one.
        Note: This blindly attempts to create it regardless of its existence.
        """
        return HBINBlock(self._buf, self._offset_next_hbin, self.parent())

    def cells(self):
        """
        Get a generator that yields each HBINCell contained in this HBIN.
        These are not necessarily in use, or linked to, from the root key.
        """
        c = HBINCell(self._buf, self._offset + 0x20, self)

        while c.offset() < self._offset_next_hbin:
            yield c
            if c.offset() + c.size() == self._offset_next_hbin:
                break
            c = c.next()

    def records(self):
        """
        Obsolete, use cells instead.
        """
        from warnings import warn
        warn("records is obsolete, use cells instead!")
        return self.cells()

class HvLEBlock(RegistryBlock):
    """
    A HvLEBlock is the log entry in a new transaction log file.
    It's length is multiple of 0x200.
    """
    def __init__(self, buf, offset, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry transaction log file.
        - `offset`: The offset into the file-like object at which the block starts.
        - `parent`: The parent block, which links to this block. The parent of the first HvLEBlock
        should be the REGFBlock, and the parents of other HvLEBlocks should be the preceding
        HvLEBlocks.
        """
        super(HvLEBlock, self).__init__(buf, offset, parent)

        _id = self.unpack_dword(0)
        if _id != 0x454C7648:
            raise ParseException("Invalid HvLE ID")

        self._offset_next_hvle = self._offset + self.size()
        self._marvin32seed = 0x82EF4D887A4E55C5

    def __str__(self):
        return "HvLE at 0x%x" % (self._offset)

    def marvin32_hash(self, buf):
        """
        Hash the buf using Marvin32 with a predefined seed.
        """
        def rotl(x, n, w):
            return (x.value << n) | (x.value >> (w - n))

        def to_uint32_le(four_bytes):
            b1, b2, b3, b4 = bytearray(four_bytes)
            return b1 | (b2 << 8) | (b3 << 16) | (b4 << 24)

        def marvin32_mix(state, val):
            lo, hi = state
            lo.value += val.value
            hi.value ^= lo.value
            lo.value = rotl(lo, 20, 32) + hi.value
            hi.value = rotl(hi, 9, 32) ^ lo.value
            lo.value = rotl(lo, 27, 32) + hi.value
            hi.value = rotl(hi, 19, 32)
            return (lo, hi)

        seed = self._marvin32seed
        lo = c_uint32(seed)
        hi = c_uint32(seed >> 32)
        state = (lo, hi)

        length = len(buf)
        pos = 0
        val = c_uint32()

        while length >= 4:
            val.value = to_uint32_le(buf[pos:pos+4])
            state = marvin32_mix(state, val)
            pos += 4
            length -= 4

        final = c_uint32(0x80)
        if length == 3:
            final.value = (final.value << 8) | buf[pos+2]
        elif length == 2:
            final.value = (final.value << 8) | buf[pos+1]
        elif length == 1:
            final.value = (final.value << 8) | buf[pos]

        state = marvin32_mix(state, final)
        state = marvin32_mix(state, c_uint32(0))
        lo, hi = state
        return (hi.value << 32 | lo.value)

    def size(self):
        """
        Get the size of this HvLEBlock.
        """
        return self.unpack_dword(0x4)

    def hash_1(self):
        """
        Get the value of Hash-1.
        """
        return self.unpack_qword(0x18)

    def calculate_hash_1(self):
        """
        Calculate the Hash-1.
        """
        return self.marvin32_hash(self._buf[self._offset+LOG_ENTRY_SIZE_HEADER:self._offset+self.size()])

    def hash_2(self):
        """
        Get the value of Hash-2.
        """
        return self.unpack_qword(0x20)

    def calculate_hash_2(self):
        """
        Calculate the Hash-2.
        """
        return self.marvin32_hash(self._buf[self._offset:self._offset+32])

    def validate_log_entry(self):
        """
        Check if this log entry is valid.
        """
        if (self.size() <= LOG_ENTRY_SIZE_HEADER) or (self.size() % LOG_ENTRY_SIZE_ALIGNMENT != 0):
            return False

        if self.hbins_size() % 0x1000 != 0:
            return False

        if self.hash_2() != self.calculate_hash_2() or self.hash_1() != self.calculate_hash_1():
            return False

        return True

    def hive_flags(self):
        """
        Get the hive flags as an unsigned integer.
        """
        return self.unpack_dword(0x8)

    def sequence(self):
        """
        Get the sequence number as an unsigned integer.
        """
        return self.unpack_dword(0xC)

    def hbins_size(self):
        """
        Get the size of all HBINBlock structures as an unsigned integer.
        """
        return self.unpack_dword(0x10)

    def dirty_pages_count(self):
        """
        Get the number of dirty pages in this log entry.
        """
        return self.unpack_dword(0x14)

    def dirty_pages_references(self):
        """
        Get a generator that yields dirty pages references in this log entry.
        """
        i = self.dirty_pages_count()
        rel_offset = 0
        while i > 0:
            c = DirtyPageReference(self._buf, self._offset + rel_offset + 0x28, self)
            yield c
            rel_offset += 8
            i -= 1

    def first_dirty_page_offset(self):
        """
        Get the offset of the first dirty page in this log entry.
        """
        return self._offset + LOG_ENTRY_SIZE_HEADER + 8*self.dirty_pages_count()

    def dirty_pages_with_references(self):
        """
        Get a generator that yields tuples with a DirtyPageReference and a DirtyPage.
        """
        current_offset = self.first_dirty_page_offset()
        for dirty_page_reference in self.dirty_pages_references():
            current_size = dirty_page_reference.size()
            dirty_page = DirtyPage(self._buf, current_offset, current_size, self)
            yield (dirty_page_reference, dirty_page)
            current_offset += dirty_page_reference.size()

    def has_next(self):
        """
        Does another HvLEBlock exist after this one?
        """
        try:
            self.next()
            return True
        except (ParseException, struct.error):
            return False

    def next(self):
        """
        Get the next HvLE after this one.
        Note: This blindly attempts to create it regardless of its existence.
        """
        return HvLEBlock(self._buf, self._offset_next_hvle, self.parent())


class DirtyPageReference(RegistryBlock):
    """
    A structure describing a single dirty page in the HvLEBlock.
    """
    def __init__(self, buf, offset, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry transaction log file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block.
        """
        super(DirtyPageReference, self).__init__(buf, offset, parent)

    def offset(self):
        """
        Offset of a dirty page in a primary file (relative from the first HBINBlock).
        """
        return self.unpack_dword(0x0)

    def size(self):
        """
        Size of a dirty page.
        """
        return self.unpack_dword(0x4)

class DirtyPage(RegistryBlock):
    """
    A a single dirty page in the HvLEBlock.
    """
    def __init__(self, buf, offset, size, parent):
        """
        Constructor.
        Arguments:
        - `buf`: Byte string containing Windows Registry transaction log file.
        - `offset`: The offset into the buffer at which the block starts.
        - `parent`: The parent block, which links to this block.
        """
        super(DirtyPage, self).__init__(buf, offset, parent)
        self._size = size

    def data(self):
        """
        Return the dirty page.
        """
        return self._buf[self._offset : self._offset + self._size]
